package be.rivendale.mathematics;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;

/**
 * <p>Represents a <a href="http://en.wikipedia.org/wiki/Triple">triple</a>, which is a set of 3 elements.
 * In this case we name the three elements x, y and z.</p>
 * <p>Naming the elements x, y and z, allows us to use this triple as a unique position in 3d space.
 * This makes our triple an ideal candidate to serve as an implementation class
 * for {@link Vector} and {@link Point}, which are themselves triples with each their own set of operations.
 */
public class Triple implements Vector, Point {
    /**
     * Represents the x coordinate.
     */
    private double x;

    /**
     * Represents the y coordinate.
     */
    private double y;

    /**
     * Represents the z coordinate.
     */
    private double z;

	/**
	 * Creates a new triple based on an array containing the X, Y and Z values.
	 * The array's X value is at index 0, the Y value is at index 1 and the Z value is at index 2.
	 * @param coordinates The array containing the coordinates X, Y and Z. This array must not be null, and must be of length 3.
	 */
	public Triple(double[] coordinates) {
		Validate.notNull(coordinates, "Creating a triple based on an array requires the array not to be null");
		Validate.isTrue(coordinates.length == 3, "Creating a triple based on an array requires the array have length three");
		this.x = coordinates[0];
		this.y = coordinates[1];
		this.z = coordinates[2];
	}

	/**
     * Creates a vector between two points.
     * A vector between two points is a vector whose length is the same as the distance between the two points
     * and has the same direction as the slope between the two points.
     * @param from The point from which the desired vector point away from. May not be null.
     * @param to The point to which the desired vector point to. May not be null.
     * @return The vector between the two specified points.
     */
    public static Vector vectorBetweenPoints(Point from, Point to) {
        Validate.notNull(to, "Creating a vector between two points can not be done if the point to which the vector points is null");
        return to.subtract(from).asVector();
    }

    /**
     * Creates a triple on the specified x, y and z coordinates.
     * @param x The x coordinate.
     * @param y The y coordinate.
     * @param z The z coordinate.
     */
    public Triple(double x, double y, double z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

	/**
	 * {@inheritDoc}
	 */
    public double getX() {
        return x;
    }

	/**
	 * {@inheritDoc}
	 */
    public double getY() {
        return y;
    }

	/**
	 * {@inheritDoc}
	 */
    public double getZ() {
        return z;
    }

	/**
	 * {@inheritDoc}
	 */
    public Vector add(Vector vector) {
        Validate.notNull(vector, "Adding two vectors can only be done if the second vector is not null");
        return new Triple(getX() + vector.getX(),
                getY() + vector.getY(),
                getZ() + vector.getZ());
    }

	/**
	 * {@inheritDoc}
	 */
    public Point add(Point point) {
        Validate.notNull(point, "Adding two points can only be done if the second point is not null");
        return (Point) add(point.asVector());
    }

	/**
	 * {@inheritDoc}
	 */
    public Point subtract(Point point) {
        Validate.notNull(point, "point may not be null when subtracting points.");
        return new Triple(getX() - point.getX(),
                getY() - point.getY(),
                getZ() - point.getZ());
    }

	/**
	 * {@inheritDoc}
	 */
    public Triple divide(double scalarDivisor) {
        return new Triple(getX() / scalarDivisor, getY() / scalarDivisor, getZ() / scalarDivisor);
    }

	/**
	 * {@inheritDoc}
	 */
    public Vector multiply(double scalarValue) {
        return new Triple(getX() * scalarValue, getY() * scalarValue, getZ() * scalarValue);
    }

	/**
	 * {@inheritDoc}
	 */
    public Vector normalize() {
        double length = length();
        if(isZeroVector()) {
            throw new IllegalArgumentException("A zero vector can not be normalized");
        }
        return divide(length);
    }

    /**
	 * {@inheritDoc}
	 */
	public double length() {
        return Math.sqrt(getX() * getX()
                + getY() * getY()
                + getZ() * getZ());
    }

	/**
	 * {@inheritDoc}
	 */
    public Vector crossProduct(Vector vector) {
        Validate.notNull(vector, "The second vector may not be null to calculate the cross product.");
        double x = getY() * vector.getZ() - getZ() * vector.getY();
        double y = getZ() * vector.getX() - getX() * vector.getZ();
        double z = getX() * vector.getY() - getY() * vector.getX();
        return new Triple(x, y, z);
    }

	/**
	 * {@inheritDoc}
	 */
    public double dotProduct(Vector vector) {
        Validate.notNull(vector, "The second vector may not be null to calculate the dot product.");
        return getX() * vector.getX()
                + getY() * vector.getY()
                + getZ() * vector.getZ();
    }

	/**
	 * {@inheritDoc}
	 */
    public double angle(Vector vector) {
        Validate.notNull(vector, "Angle between two vectors can not be determined if the second vector is null");
        Validate.isTrue(!isZeroVector() && !vector.isZeroVector(), "Neither of the two vectors may be zero vectors to calculate the angle between them");
        return Math.acos(dotProduct(vector) / (length() * vector.length()));
    }

	/**
	 * {@inheritDoc}
	 */
    public boolean isZeroVector() {
       return this.equals(ZERO_VECTOR);
    }

	/**
	 * {@inheritDoc}
	 */
    public boolean isNormalized() {
        return MathematicalUtilities.equals(length(), 1);
    }

	/**
	 * {@inheritDoc}
	 */
    public boolean isPerpendicularTo(Vector vector) {
        Validate.notNull(vector, "Unable to determine if two vectors are perpendicular if the specified vector is null");
        return MathematicalUtilities.equals(angle(vector), Math.PI / 2);
    }

    /**
     * {@inheritDoc}
     */
    public boolean isParallelTo(Vector vector) {
        Validate.notNull(vector, "Unable to determine if two vectors are parallel if the specified vector is null");
        return MathematicalUtilities.equals(angle(vector), 0);
    }

	/**
	 * {@inheritDoc}
	 */
    public double scalarTripleProduct(Vector b, Vector c) {
        return crossProduct(b).dotProduct(c);
    }

	/**
	 * {@inheritDoc} 
	 */
	public Vector project(Vector normalizedVectorOnWhichToProject) {
		Validate.notNull(normalizedVectorOnWhichToProject, "Vector on which to project is required");
		Validate.isTrue(normalizedVectorOnWhichToProject.isNormalized(), "Vector on which to project is must be normalized (a unit vector)");
		return normalizedVectorOnWhichToProject.multiply(normalizedVectorOnWhichToProject.dotProduct(this) / normalizedVectorOnWhichToProject.length());
	}

	/**
	 * {@inheritDoc}
	 */
    public Point asPoint() {
        return this;
    }

	/**
	 * {@inheritDoc}
	 */
    public Vector invert() {
        return this.multiply(-1);
    }

    public Vector asVector() {
        return this;
    }

    /**
     * Tests if two vectors are equal.
	 * Two vectors are considered equal when they have the same x, y and z coordinates.
     * @see Object#equals(Object)
     */
    @Override
    public boolean equals(Object object) {
        Vector vector = (Vector)object;
        return MathematicalUtilities.equals(getX(), vector.getX())
                && MathematicalUtilities.equals(getY(), vector.getY())
                && MathematicalUtilities.equals(getZ(), vector.getZ());
    }

    /**
     * Converts this point to a string in the form of <code>(x, y, z)</code>
     * The <code>'.'</code> character is used as a decimal separator for each of the coordinates:
     * @return The representation of this point as a string.
     */
    public String toString() {
        String decimalFormatPattern = "0." + StringUtils.repeat("0", MathematicalUtilities.NUMBER_OF_SYMBOLS_AFTER_DECIMAL_SEPARATOR);
        DecimalFormat format = new DecimalFormat(decimalFormatPattern);
        format.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.UK));
        return "(" + format.format(getX()) + ", " + format.format(getY()) + ", " + format.format(getZ()) + ")";
    }
}
